[アップデート] AWS Network Firewall のステートフルルールグループが TCP トラフィックの reject action をサポートしました
通信できない場合タイムアウトまで待ちたくないな
こんにちは、のんピ(@non____97)です。
皆さんはAWS Network Firewallで通信をさせない場合にタイムアウトまで待たせたくないと思ったことはありますか? 私はあります。
従来からあるdrop actionだと通信を通さず、送信元に対してレスポンスを何も返しません。そのため、送信元からすると通信が何らかの原因で到達していないのか、それともどこかでブロックされているのか判断することが難しいです。
また、送信元は送信先からのレスポンスがあるまでポートをオープンして待つことになるので、時間的にもリソース的にも勿体無いです。
少し前ですが、AWS Network Firewall のステートフルルールグループが TCP トラフィックの reject action をサポートしました。
これにより、送信元に通信が拒否されたことを伝えることができます。
dropが良いのかrejectが良いのかは考え方によるとは思いますが、選択肢が増えたのは良いことです。
実際に試してみたので紹介します。
いきなりまとめ
- 5-tupleとSuricata互換のIPSルールのルールグループにおいて、TCP トラフィックのreject actionが追加された
- ただし、2023/3/10時点ではFTPとIMAPはサポートしていない
- ドメインリストのルールグループには設定できない
- ドメインに対してreject actionを設定したい場合は、Suricata互換のIPSルールで行う
- reject actionのルールにマッチすると、RSTフラグが付与されたパケットが返ってくる
- Network Firewallが出力したログからはdropなのか、rejectなのか判別することはできない
signature
やsignature_id
から判断する必要がある
ドキュメントの確認
まずはreject actionについてAWS公式ドキュメントから確認してみます。
Reject – Drop traffic that matches the conditions of the stateful rule and send a TCP reset packet back to sender of the packet. A TCP reset packet is a packet with no payload and a RST bit contained in the TCP header flags. Reject is available only for TCP traffic. This option doesn't support FTP and IMAP protocols.
(以下機械翻訳)
Reject - ステートフルルールの条件に一致するトラフィックをドロップし、パケットの送信者にTCPリセットパケットを返送します。TCPリセットパケットとは、ペイロードがなく、TCPヘッダーフラグにRSTビットが含まれるパケットです。Reject は、TCP トラフィックに対してのみ有効です。このオプションは、FTPとIMAPプロトコルをサポートしません。
Defining rule actions in AWS Network Firewall - AWS Network Firewall
reject actionを設定したルールに一致すると、RSTフラグが付与されたパケットが返ってくるようですね。
Suricataのドキュメントも確認します。
- reject - send RST/ICMP unreach error to the sender of the matching packet.
- rejectsrc - same as just reject
- rejectdst - send RST/ICMP error packet to receiver of the matching packet.
- rejectboth - send RST/ICMP error packets to both sides of the conversation.
reject
を指定すると、RSTだけではなくICMPメッセージも返すように見えますね。
検証環境
実際にreject actionを試してみます。
検証環境は以下の通りです。
以下ルールグループについて、reject actionを設定できるか確認します。
- 5-tuple
- ドメインリスト
- Suricata互換のIPSルール
RSTフラグが付与されたTCPパケットが返ってきたかはtcpdumpによるパケットキャプチャーで確認します。その際、以下記事を参考にWiresharkにパケットキャプチャーの結果を転送します。
tcpdumpの結果を転送する際は、EC2インスタンスのTCP/22に対してSSMセッションマネージャーでポートフォワーディングをして、SSHのリモートコマンドでtcpdumpを実行します。
検証環境はAWS CDKでデプロイします。使用したコードは以下リポジトリに保存しています。
npx cdk deploy
でリソースをデプロイすると、以下のようなOutputs
が出力されるので控えておきます。
Outputs: NetworkFirewallStack.Ec2InstanceAGetSecretKeyCommand9D38ABDA = aws ssm get-parameter --name /ec2/keypair/key-07b974428572e521f --region us-east-1 --with-decryption --query Parameter.Value --output text > ./key-pair/test-key-pair.pem NetworkFirewallStack.Ec2InstanceAInstanceId6D8E6BA7 = i-08c44ef77a0db8bfe NetworkFirewallStack.Ec2InstanceAInstancePrivateIpAddress5C73F3CE = 10.1.1.37
drop actionの確認
Wiresharkの設定
reject actionの挙動を実際に確認する前に、drop actionの挙動を確認しておきます。
下準備としてWiresharkの設定をします。
まず、AWS CDKデプロイした時のOutputs
に出力されたコマンドを実行して、EC2インスタンスに設定したキーペアの秘密鍵をダウンロードします。
# EC2インスタンスに設定したキーペアの秘密鍵のダウンロード $ aws ssm get-parameter --name /ec2/keypair/key-07b974428572e521f --region us-east-1 --with-decryption --query Parameter.Value --output text > ./key-pair/test-key-pair.pem # 権限を 400 に設定 $ chmod 400 ./key-pair/test-key-pair.pem
次にWiresharkのリモートキャプチャー用の設定ファイルを編集します。
設定箇所は以下の2箇所です。
target_private_ipaddr
- tcpdumpを実行するEC2インスタンスのIPアドレス
- SSHの通信がパケットキャプチャーのノイズとなるため指定
portforward_instance_id
- SSMセッションマネージャーでポートフォワーディングするEC2インスタンスのID
どちらもAWS CDKデプロイした時のOutputs
に出力されているので、そちらを設定します。
filter
やportforward_port
などはお好みで変更してください。
############################################################################### # Target Host Setting # # EC2インスタンスのキーペア declare -r key_pair=./key-pair/test-key-pair.pem # EC2インスタンスにSSHする際のユーザ名 declare -r ssh_user_name=ec2-user # 接続先EC2インスタンスのホスト名 # PublicのIPアドレスでアクセスできる場合は、EC2のPublic DNSを指定する # ポートフォワーディングする場合はlocalhostを指定する declare -r connect_hostname=localhost ############################################################################### # Capture Setting # # チャプチャーインタフェース declare -r interface=eth0 # キャプチャーIPアドレス(EC2インスタンスのPrivate IPアドレス) # e.g.) declare -r target_private_ipaddr=10.1.1.4 declare -r target_private_ipaddr=10.1.1.37 # キャプチャーフィルタ # (通信が大量にある場合は設定することが必須) # e.g.) FILTER="tcp port 80" local filter="tcp port 80 or icmp" ############################################################################### # Portforward Setting (Optional) # # ポートフォワーディングする場合はローカルの空きポート番号を指定する(1024番以上) # ポートフォワーディングを利用しない場合はコメントアウトする declare -r portforward_port=10022 # 接続先EC2インスタンスのID # SSMセッションマネージャーでポートフォワーディングする際に使用 # ポートフォワーディングを利用しない場合はコメントアウトする # e.g.) declare -r portforward_instance_id=i-xxxxxxxxxxxxxxxxx declare -r portforward_instance_id=i-08c44ef77a0db8bfe
設定が完了したら、./wireshark/wireshark-remote.sh
を実行します。これにより、SSMセッションマネージャーでTCP/22をローカルマシンのTCP/10022にポートフォワーディングして、TCP/10022に対してSSH経由でtcpdumpを実行してくれます。
$ ./wireshark/wireshark-remote.sh Starting session with SessionId: botocore-session-1678431778-0c14fa7f5f8aade49 Port 10022 opened for sessionId botocore-session-1678431778-0c14fa7f5f8aade49. Waiting for connections... Connection accepted for session [botocore-session-1678431778-0c14fa7f5f8aade49] ** (wireshark:97345) 16:03:16.238702 [GUI WARNING] -- Populating font family aliases took 176 ms. Replace uses of missing font family ".AppleSystemUIFont" with one that exists to avoid this cost. tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
また、自動でWiresharkが起動します。-
というインターフェースを選択しておきます。
これで下準備はOKです。
ちなみに、./wireshark/wireshark-remote.sh
は以下の通りです。パケットキャプチャーを終了したい場合はCtrl + C
です。SSMセッションマネージャーのポートフォワーディングやWiresharkのプロセスも一緒に落ちるようにしています。
#!/bin/bash # Name # wireshark-remote # # format # wireshark-remote.sh [-c config_path] # # description # ssh経由でtcpdumpを実行した結果をwiresharkに転送することによって # 間接的にwiresharkによるリモートキャプチャーする # # requirements # 1. !requiretty設定 # リモートホスト(EC2)でtcpdumpを実行するユーザ(ec2-user)に # コマンド(tcpdump)を実行するときにTTYを要求しない設定を追加する # 例.ec2-userの場合、visudoにて以下の行を追加 # Defaults:ec2-user !requiretty # # 2. Wiresharのキャプチャー開始 # このシェルスクリプトを実行して起動したWiresharkのインターフェイスで - を選択する # # return # 0 = SUCCESS # 0 != FAILED # set -euo pipefail # kill child process trap 'pkill -P $$' HUP trap 'pkill -P $$' INT trap 'pkill -P $$' TERM ############################################################################## # Main # function main () { declare -r snaplen=262144 declare -r keep_alive=60 local portforward_option local config local result # include config. (default: wireshark-remote.conf) if [[ "$#" == 2 && "$1" == "-c" ]]; then config="$2" else config="./wireshark/wireshark-remote.conf" fi # load config if [[ ! -f "${config}" ]]; then echo "Invalid config file : ${config}" exit 1 fi source "${config}" # Portforwarding as child process. if [[ -n "${portforward_port}" ]]; then aws ssm start-session --target "${portforward_instance_id}" \ --document-name AWS-StartPortForwardingSession \ --parameters '{"portNumber":["22"],"localPortNumber":["'"${portforward_port}"'"]}' & portforward_option="-p ${portforward_port}" fi sleep 5 # set Filter if [[ -n "${filter}" ]]; then filter="and \(${filter}\)" fi # remote command ssh \ -i "${key_pair}" \ -C \ -o ServerAliveInterval="${keep_alive}" \ -o StrictHostKeyChecking=no \ "${ssh_user_name}@${connect_hostname}" \ "${portforward_option}" \ sudo tcpdump \ -U \ -i "${interface}" \ -s "${snaplen}" \ -w - \ "not \(host ${target_private_ipaddr} and tcp port 22\) ${filter}" \ | wireshark -i - result=$? if (( ${result} == 0 )); then pkill -P $$ fi exit ${result} } main "$@"
パケットキャプチャーの確認
下準備ができたので、実際にパケットキャプチャーをしてdrop actionの挙動を確認してみます。
定義したルールグループのルールは以下の通りです。HTTP/80に対してdropするようにしています。
$ aws network-firewall describe-rule-group \ --rule-group-name network-firewall-rule-group-5-tuple \ --type STATEFUL \ --query "RuleGroup.RulesSource" { "StatefulRules": [ { "Action": "DROP", "Header": { "Protocol": "HTTP", "Source": "$HOME_NET", "SourcePort": "ANY", "Direction": "FORWARD", "Destination": "$EXTERNAL_NET", "DestinationPort": "80" }, "RuleOptions": [ { "Keyword": "msg", "Settings": [ "\"HTTP drop\"" ] }, { "Keyword": "sid", "Settings": [ "1000001" ] }, { "Keyword": "rev", "Settings": [ "1" ] } ] } ] }
この状態で、EC2インスタンスからHTTP/80の通信をしようとしてみます。
$ curl -m 5 -I http://dev.classmethod.jp curl: (28) Operation timed out after 5000 milliseconds with 0 bytes received
タイムアウトとなりましたね。
Wiresharkでこの時の通信を確認してみます。
TCPのコネクション接続時にSYN + ACK
が帰ってきていますが、HEAD / HTTP/1.1
のリクエストに対するレスポンスが返ってきていないですね。最終的には諦めのFIN + ACK
がクライアントから投げられています。
CloudWatch Logsに出力されたログを確認すると、以下のようなログが出力されていました。
{ "firewall_name": "network-firewall", "availability_zone": "us-east-1a", "event_timestamp": "1678432026", "event": { "app_proto": "http", "src_ip": "10.1.1.37", "src_port": 37954, "event_type": "alert", "alert": { "severity": 3, "signature_id": 1000001, "rev": 1, "signature": "HTTP drop", "action": "blocked", "category": "" }, "flow_id": 18983069345273, "dest_ip": "76.223.57.58", "proto": "TCP", "http": { "hostname": "dev.classmethod.jp", "url": "/", "http_user_agent": "curl/7.88.1", "http_method": "HEAD", "protocol": "HTTP/1.1", "length": 0 }, "dest_port": 80, "timestamp": "2023-03-10T07:07:06.324930+0000" } }
reject actionの確認
5-tuple ルールグループの場合
それでは、reject actionの挙動を確認していきます。
まずは、5-tuple ルールグループの場合です。
定義したルールグループのルールは以下の通りです。HTTP/80に対してrejectするようにしています。
$ aws network-firewall describe-rule-group \ --rule-group-name network-firewall-rule-group-5-tuple \ --type STATEFUL \ --query "RuleGroup.RulesSource" { "StatefulRules": [ { "Action": "REJECT", "Header": { "Protocol": "HTTP", "Source": "$HOME_NET", "SourcePort": "ANY", "Direction": "FORWARD", "Destination": "$EXTERNAL_NET", "DestinationPort": "80" }, "RuleOptions": [ { "Keyword": "msg", "Settings": [ "\"HTTP reject\"" ] }, { "Keyword": "sid", "Settings": [ "1000002" ] }, { "Keyword": "rev", "Settings": [ "1" ] } ] } ] }
この状態で、EC2インスタンスからHTTP/80の通信をしようとしてみます。
$ curl -m 5 -I http://dev.classmethod.jp curl: (56) Recv failure: Connection reset by peer
タイムアウトではなく、接続先からコネクションがリセットされたとメッセージが出力されました。まさにrejectです。一般利用者からしたら何らかの原因で拒否されたことが分かりやすいですね。
Wiresharkでこの時の通信を確認してみます。
しっかりとRST + ACK
がレスポンスとして返ってきていますね。なお、ICMPのパケットは特に発生していないようでした。
CloudWatch Logsに出力されたログを確認すると、以下のようなログが出力されていました。
{ "firewall_name": "network-firewall", "availability_zone": "us-east-1a", "event_timestamp": "1678433301", "event": { "app_proto": "http", "src_ip": "10.1.1.37", "src_port": 49596, "event_type": "alert", "alert": { "severity": 3, "signature_id": 1000002, "rev": 1, "signature": "HTTP reject", "action": "blocked", "category": "" }, "flow_id": 2156274843503677, "dest_ip": "13.248.175.13", "proto": "TCP", "http": { "hostname": "dev.classmethod.jp", "url": "/", "http_user_agent": "curl/7.88.1", "http_method": "HEAD", "protocol": "HTTP/1.1", "length": 0 }, "dest_port": 80, "timestamp": "2023-03-10T07:28:21.711137+0000" } }
action
がdrop actionの時と同じくblocked
でした。dropなのかrejectなのかはsignature
やsignature_id
から判断する必要があるようです。
ドメインリスト ルールグループの場合
次に、ドメインリスト ルールグループの場合を試します。
ドメインリストはALLOWLIST
かDENYLIST
かの2択です。REJECTLIST
のようなものはないので、恐らくrejectはできないと思いますが、試してみます。
定義したルールグループのルールは以下の通りです。HTTPでのdev.classmethod.jp
に対して拒否するようにしています。
$ aws network-firewall describe-rule-group \ --rule-group-name network-firewall-rule-group-domain-list \ --type STATEFUL \ --query "RuleGroup.RulesSource" { "RulesSourceList": { "Targets": [ "dev.classmethod.jp" ], "TargetTypes": [ "HTTP_HOST" ], "GeneratedRulesType": "DENYLIST" } }
この状態で、EC2インスタンスからdev.classmethod.jp
に対して、HTTP/80の通信をしようとしてみます。
$ curl -m 5 -I http://dev.classmethod.jp curl: (28) Operation timed out after 5000 milliseconds with 0 bytes received
接続断ではなく、タイムアウトとなりました。
Wiresharkでこの時の通信を確認してみます。
先に検証したdrop actionの時と全く同じ挙動で、HEAD / HTTP/1.1
のリクエストに対するレスポンスが返ってこず。最終的にはFIN + ACK
をクライアントから投げていますね。
CloudWatch Logsに出力されたログを確認すると、以下のようなログが出力されていました。
{ "firewall_name": "network-firewall", "availability_zone": "us-east-1a", "event_timestamp": "1678433702", "event": { "tx_id": 0, "app_proto": "http", "src_ip": "10.1.1.37", "src_port": 53886, "event_type": "alert", "alert": { "severity": 1, "signature_id": 1, "rev": 1, "signature": "matching HTTP denylisted FQDNs", "action": "blocked", "category": "" }, "flow_id": 267369727812699, "dest_ip": "76.223.57.58", "proto": "TCP", "http": { "hostname": "dev.classmethod.jp", "url": "/", "http_user_agent": "curl/7.88.1", "http_method": "HEAD", "protocol": "HTTP/1.1", "length": 0 }, "dest_port": 80, "timestamp": "2023-03-10T07:35:02.160065+0000" } }
Suricata互換のIPSルール ルールグループの場合
最後にSuricata互換のIPSルール ルールグループの場合を試します。
定義したルールグループのルールは以下の通りです。HTTPでのdev.classmethod.jp
に対して拒否するようにしています。
aws network-firewall describe-rule-group \ --rule-group-name network-firewall-rule-group-suricata \ --type STATEFUL \ --query "RuleGroup.RulesSource" { "RulesString": "reject http $HOME_NET any -> $EXTERNAL_NET 80 (http.host; dotprefix; content:\"dev.classmethod.jp\"; endswith; msg:\"Reject HTTP domain\"; sid:1000011; rev:1;)" }
この状態で、EC2インスタンスからdev.classmethod.jp
に対して、HTTP/80の通信をしようとしてみます。
$ curl -m 5 -I http://dev.classmethod.jp curl: (56) Recv failure: Connection reset by peer
タイムアウトではなく、接続がリセットされましたね。
Wiresharkでこの時の通信を確認してみます。
RST + ACK
がレスポンスとして返ってきていますね。
CloudWatch Logsに出力されたログを確認すると、以下のようなログが出力されていました。
{ "firewall_name": "network-firewall", "availability_zone": "us-east-1a", "event_timestamp": "1678434721", "event": { "tx_id": 0, "app_proto": "http", "src_ip": "10.1.1.37", "src_port": 49082, "event_type": "alert", "alert": { "severity": 3, "signature_id": 1000011, "rev": 1, "signature": "Reject HTTP domain", "action": "blocked", "category": "" }, "flow_id": 979093120163954, "dest_ip": "76.223.57.58", "proto": "TCP", "http": { "hostname": "dev.classmethod.jp", "url": "/", "http_user_agent": "curl/7.88.1", "http_method": "HEAD", "protocol": "HTTP/1.1", "length": 0 }, "dest_port": 80, "timestamp": "2023-03-10T07:52:01.863940+0000" } }
TCPに対する drop/reject actionの違い
drop actionの場合
先ほどはHTTP/80に対する drop/reject actionの違いを確認しました。
せっかくなので、TCP/80に対するdrop/reject actionも確認します。
まずはdrop actionからです。
定義したルールグループのルールは以下の通りです。TCP/80に対してdropするようにしています。
$ aws network-firewall describe-rule-group \ --rule-group-name network-firewall-rule-group-5-tuple \ --type STATEFUL \ --query "RuleGroup.RulesSource" { "StatefulRules": [ { "Action": "DROP", "Header": { "Protocol": "TCP", "Source": "$HOME_NET", "SourcePort": "ANY", "Direction": "FORWARD", "Destination": "$EXTERNAL_NET", "DestinationPort": "80" }, "RuleOptions": [ { "Keyword": "msg", "Settings": [ "\"TCP drop\"" ] }, { "Keyword": "sid", "Settings": [ "1000001" ] }, { "Keyword": "rev", "Settings": [ "1" ] } ] } ] }
この状態で、EC2インスタンスからHTTP/80の通信をしようとしてみます。
$ curl -I -m 5 http://dev.classmethod.jp curl: (28) Connection timed out after 5000 milliseconds
タイムアウトになりましたね。
Wiresharkでこの時の通信を確認してみます。
TCP/80に対するSYNのレスポンスがないことが分かります。
CloudWatch Logsに出力されたログを確認すると、以下のようなログが出力されていました。
{ "firewall_name": "network-firewall", "availability_zone": "us-east-1a", "event_timestamp": "1678521174", "event": { "src_ip": "10.1.1.47", "src_port": 45102, "event_type": "alert", "alert": { "severity": 3, "signature_id": 1000001, "rev": 1, "signature": "TCP drop", "action": "blocked", "category": "" }, "flow_id": 315433996915730, "dest_ip": "13.248.175.13", "proto": "TCP", "dest_port": 80, "timestamp": "2023-03-11T07:52:54.398354+0000" } }
reject actionの場合
続いて、reject actionの場合です。
定義したルールグループのルールは以下の通りです。TCP/80に対してrejectするようにしています。
$ aws network-firewall describe-rule-group \ --rule-group-name network-firewall-rule-group-5-tuple \ --type STATEFUL \ --query "RuleGroup.RulesSource" { "StatefulRules": [ { "Action": "REJECT", "Header": { "Protocol": "TCP", "Source": "$HOME_NET", "SourcePort": "ANY", "Direction": "FORWARD", "Destination": "$EXTERNAL_NET", "DestinationPort": "80" }, "RuleOptions": [ { "Keyword": "msg", "Settings": [ "\"TCP reject\"" ] }, { "Keyword": "sid", "Settings": [ "1000004" ] }, { "Keyword": "rev", "Settings": [ "1" ] } ] } ] }
この状態で、EC2インスタンスからHTTP/80の通信をしようとしてみます。
$ curl -I -m 5 http://dev.classmethod.jp curl: (7) Failed to connect to dev.classmethod.jp port 80 after 202 ms: Couldn't connect to server
202msでサーバーに接続できなかったと返してくれました。
Wiresharkでこの時の通信を確認してみます。
TCP/80に対するSYNのRST/ACKが返ってきていることが分かります。
CloudWatch Logsに出力されたログを確認すると、以下のようなログが出力されていました。
{ "firewall_name": "network-firewall", "availability_zone": "us-east-1a", "event_timestamp": "1678521541", "event": { "src_ip": "10.1.1.47", "src_port": 32818, "event_type": "alert", "alert": { "severity": 3, "signature_id": 1000004, "rev": 1, "signature": "TCP reject", "action": "blocked", "category": "" }, "flow_id": 1045219831571461, "dest_ip": "76.223.57.58", "proto": "TCP", "dest_port": 80, "timestamp": "2023-03-11T07:59:01.844805+0000" } }
ポリシーに応じてdropとrejectを使い分けよう
AWS Network Firewall のステートフルルールグループが TCP トラフィックの reject action をサポートしたアップデートを紹介しました。
reject actionを設定することでタイムアウトまでわざわざ待つ必要がなくなります。
dropとrejectはポリシーに応じて使い分けましょう。
よくdropがrejectよりも良いというのは聞きますね。これは、以下のような理由かなと推測します。
- rejectはわざわざ送信元に通知してしまう関係上、宛先が存在していることを伝えることになるため、dropの方が良い
- dropの方がポートスキャンまでに時間がかかる
1つ目の理由はネットワーク型IPSが普及している現在、あまり関係ないかなと考えます。攻撃者もネットワーク型IPSの存在は考えていると推測します。
2つ目の理由についても攻撃者がポートスキャンする場合、元々タイムアウトまで悠長に待つこともないのかなと思います。
じゃあ、dropよりもrejectが良いのかと言うとそれも違います。
rejectにすることでRSTパケットが流れる分、ネットワークに流れる通信の流量は僅かに増えます。ここで、「タイムアウトになるまで通信を打ち続けたりするよりかは量が少ないのでは?」と思われるかもしれません。確かにそれも一理あるのですが、UDPリフレクションのように偽装したIPアドレスに対してRST/ACKパケットを大量に送りつけられる可能性もあります。RST/ACKのパケットサイズは小さいですが、攻撃者と思われる通信にわざわざ「通信できませんでした」と送り返す必要もないと考えます。
そのため、個人的には内部から外部への通信はrejectで、外部から内部への通信はdropみたいな考え方でも良いんじゃないかなと思います。
この記事が誰かの助けになれば幸いです。
以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!